// Copyright (c) Microsoft Corporation. All rights reserved.

using System;
using System.Collections.Generic;
using System.Text;
using System.Diagnostics;
using System.Globalization;
using Microsoft.TeamFoundation.Migration.Toolkit.VC;
using Microsoft.TeamFoundation.Migration.Toolkit;
using Microsoft.TeamFoundation.VersionControl.Client;
using Microsoft.TeamFoundation.VersionControl.Common;
using Microsoft.TeamFoundation.Client;

namespace Microsoft.Vsts.Rangers.Migration.TfsToTfs
{
    /// <summary>
    /// The TFS-TFS migration engine
    /// </summary>
    class TfsToTfsAnalysisEngine : TfsToSourceAnalysisEngine
    {
        private string m_CommentModifier;
        VersionControlServer tfsClient;
        TfsToTfsAnalysisAlgorithms m_algorithms;
        HighWaterMark<int> m_hwm;

        /// <summary>
        /// Constructor
        /// </summary>
        /// <param name="session">Session that this analysis engine belongs to</param>
        /// <param name="manager">Changegrouping manager of the analysis engine</param>
        public TfsToTfsAnalysisEngine(Session session, ChangeGroupingMananger manager)
            : base(session, manager)
        {
            string serverKey = ((TfsVersionControlTargetEndpoint)session.Source).Server;



            TeamFoundationServer tfsServer = AuthenticationHelper.GetServerInstance(serverKey);

            tfsClient = (VersionControlServer)tfsServer.GetService(typeof(VersionControlServer));

            detectParentRenameOnMappings(session);

            m_algorithms = new TfsToTfsAnalysisAlgorithms(session);

            int defaultHwm = loadEarliestHwmBasedOnMappings(0, session);
            if (manager.Direction == MigrationDirection.SourceToTfs)
            {
                m_hwm = new HighWaterMark<int>(session, ResourceStrings.TfsForwardAnalysisHWM, defaultHwm);
            }
            else
            {
                m_hwm = new HighWaterMark<int>(session, ResourceStrings.TfsBackwardAnalysisHWM, defaultHwm);
            }
            m_hwm.BeforeUpdate += m_hwm_BeforeUpdate;

            if (m_hwm.Value < defaultHwm)
            {
                m_hwm.Update(defaultHwm);
            }

            manager.LastHighWaterMark = m_hwm;

            m_CommentModifier = session.GetValue<string>("MigrationChangeCommentModifier", ResourceStrings.TfsDefaultCommentModifier);
        }

        private void detectParentRenameOnMappings(Session session)
        {
            string highWaterMark;
            if (Manager.Direction == MigrationDirection.SourceToTfs)
            {
                highWaterMark = SessionVariables.GetSessionVariable(session.Id, ResourceStrings.TfsForwardAnalysisHWM); 
            }
            else
            {
                highWaterMark = SessionVariables.GetSessionVariable(session.Id, ResourceStrings.TfsBackwardAnalysisHWM);
            }
            if (!string.IsNullOrEmpty(highWaterMark))
            {
                //We only check parent rename on mappings for the first time an analysis engine is started on the session.
                return;
            }

            foreach (VersionControlMapping m in session.Mappings)
            {
                string currentItem = VersionControlPath.GetFolderName(m.Source);
                while (VersionControlPath.GetFolderDepth(currentItem) > 1 )
                {
                    try
                    {
                        foreach (Changeset changeset in tfsClient.QueryHistory(currentItem,
                                VersionSpec.Latest,
                                0,
                                RecursionType.None,
                                null,
                                null,
                                null,
                                Int32.MaxValue,
                                true,
                                false))
                        {
                            foreach (Change change in changeset.Changes)
                            {
                                if (((change.ChangeType & ChangeType.Rename) == ChangeType.Rename)
                                    && VersionControlPath.Equals(change.Item.ServerItem, currentItem))
                                {
                                    throw new MigrationException(
                                        string.Format(ResourceStrings.Culture,
                                            ResourceStrings.ParentRenameOnMappedPath, m.Source, change.Item.ServerItem, changeset.ChangesetId));
                                }
                            }
                        }
                    }
                    catch (ItemNotFoundException)
                    {
                        // Item does not exist, continue. 
                    }
                    currentItem = VersionControlPath.GetFolderName(currentItem);
                }
            }
        }
        private int loadEarliestHwmBasedOnMappings(int notBefore, VersionControlSession session)
        {
            //Borrowed from TfstoWssAnalysisEngine with the change to slot mode
            int currentOldest = tfsClient.GetLatestChangesetId();

            if (notBefore >= currentOldest)
            {
                return notBefore;
            }

            VersionSpec earliest = (notBefore > 0) ? new ChangesetVersionSpec(notBefore) : null;


            foreach (VersionControlMapping m in session.Mappings)
            {
                try
                {
                    bool slotmode = session.GetValue<bool>("UseSlotmodeMappings", true);

                    while (true)
                    {
                        bool foundChanges = false;
                        string serverPath = m.Source;

                        foreach (Changeset latest in tfsClient.QueryHistory(serverPath,
                            VersionSpec.Latest,
                            0,
                            RecursionType.Full,
                            null,
                            earliest,
                            new ChangesetVersionSpec(currentOldest),
                            256,
                            false,
                            slotmode))
                        {
                            // we want to use the oldest starting point - if result is newer then use the older
                            if (currentOldest >= latest.ChangesetId)
                            {
                                foundChanges = true;

                                currentOldest = latest.ChangesetId;
                            }

                            /* decrement once more to ensure we go far enough back
                            * the scenario we need to be careful of is when the root directory
                            * and a child item(s) are created in the same changeset we still want 
                            * to migrate the child item(s) so we need to go to the change just prior
                            * to them.
                            * 
                            * We want to decrement whether or not we found a change because if the latest
                            * checkin is the checkin that created the migration root we still need to
                            * go down one.
                            */

                            currentOldest--;
                        }

                        if (!foundChanges)
                        {
                            break;
                        }
                    }
                }
                catch (ItemNotFoundException)
                {
                    // the root does not need to exist prior to migration
                }
            }

            return (currentOldest < notBefore) ? notBefore : currentOldest;
        }

        void m_hwm_BeforeUpdate(object sender, HighWaterMarkUpdatedEventArgs<int> args)
        {
            int newHwm = args.New;
            int currentHwm = args.Current;

            // we only move forward
            Debug.Assert(newHwm >= currentHwm);

            // we never go negative
            Debug.Assert(newHwm >= 0);

            if (newHwm < currentHwm)
            {
                throw new MigrationException("High water mark should not go backwards");
            }

            if (newHwm < 0)
            {
                throw new MigrationException("High water mark must be greater than zero");
            }
        }

        /// <summary>
        /// Execute the analysis engine
        /// </summary>
        public void Run()
        {
            m_hwm.Reload();

            string skipComment = Session.GetValue<string>("SkipChangeComment", null);


            for (Changeset changeset = getNextTfsChange();
                changeset != null;
                changeset = getNextTfsChange())
            {
                TraceManager.TraceInformation("Analyzing TFS change {0}", changeset.ChangesetId);

                if (TfsUtil.IsOurTfsChange(changeset, m_CommentModifier))
                {
                    TraceManager.TraceInformation("Skipping mirrored change {0}", changeset.ChangesetId);

                    // skipping changeset we submitted
                    m_hwm.Update(changeset.ChangesetId);
                    continue;
                }

                if (!string.IsNullOrEmpty(skipComment))
                {
                    if (changeset.Comment != null && changeset.Comment.Contains(skipComment))
                    {
                        TraceManager.TraceInformation("Changeset {0} contains the skip comment {1}",
                            changeset.ChangesetId,
                            skipComment);

                        // skip based on skip comment
                        m_hwm.Update(changeset.ChangesetId);
                        continue;
                    }
                }

                changeset = getChangesetWithChangeInfo(changeset);

                if (changeContainsMappedItems(changeset))
                {
                    changeset = getChangesetWithDownloadInfo(changeset);
                    int actions = Process(changeset);

                    TraceManager.TraceInformation("Created {0} actions for TFS change {1}",
                        actions, changeset.ChangesetId);
                }
                else
                {
                    TraceManager.TraceInformation("Changeset {0} contains no mapped items", changeset.ChangesetId);
                }

                m_hwm.Update(changeset.ChangesetId);

                Manager.PromoteAnalysisToPending();
            }
        }

        /// <summary>
        /// This is a perf optimization in the case of only on mapping in the session.  
        /// If there is only one mapping we only need to consider the changeids that
        /// occur under that mapping.  If there is more than one then we'd need to make multiple round-trips to the 
        /// server so we default to the base case.
        /// </summary>
        /// <returns>The next changeset ID to consider</returns>
        int getNextChangeId()
        {
            int nextId = m_hwm.Value + 1;

            if (Session.Mappings.Count == 1)
            {
                nextId = loadEarliestHwmBasedOnMappings(nextId, Session);
            }

            return nextId;
        }

        private Changeset getNextTfsChange()
        {
            int nextChange = getNextChangeId();

            while (true)
            {
                try
                {
                    return tfsClient.GetChangeset(nextChange, false, false);
                }
                catch (ChangesetNotFoundException)
                {
                    // in v1.0 this means that we have hit the end of the change chain but in v-Next
                    // there might be gaps in the sequence.  When that time comes adding a call to
                    // tfsClient.GetLatestChangesetId would go here.

                    return null;
                }
                catch (ResourceAccessException)
                {
                    // No access to any item in change
                    m_hwm.Update(nextChange);

                    // no access to any item in change
                    nextChange = getNextChangeId();
                }
            }

        }

        private Changeset getChangesetWithChangeInfo(Changeset changeset)
        {
            return tfsClient.GetChangeset(changeset.ChangesetId, true, false);
        }

        private Changeset getChangesetWithDownloadInfo(Changeset changeset)
        {
            return tfsClient.GetChangeset(changeset.ChangesetId, true, true);
        }

        protected override TfsAnalysisAlgorithms LoadAnalysisAlgorithms()
        {
            return m_algorithms;
        }

        protected override ChangeGrouping CreateChangeGrouping(Changeset changeSet)
        {
            ChangeGrouping group = Manager.Create(changeSet.ChangesetId.ToString(CultureInfo.InvariantCulture));
            group.Name = changeSet.ChangesetId.ToString(CultureInfo.InvariantCulture);
            group.Owner = changeSet.Owner;
            group.Comment = changeSet.Comment;
            group.ChangeTimeUtc = changeSet.CreationDate.ToUniversalTime();
            group.Status = ChangeStatus.Analysis;
            group.SessionId = Session.Id;
            group.ExecutionOrder = changeSet.ChangesetId;

            return group;
        }

        protected override VersionControlMapping FindChangeMapping(Change change)
        {
            return Util.FindAppropriateMappingForServerPath(change.Item.ServerItem, Session);
        }

        private bool changeContainsMappedItems(Changeset changeset)
        {
            foreach (Change change in changeset.Changes)
            {
                if (Util.FindAppropriateMappingForServerPath(change.Item.ServerItem, Session) != null)
                {
                    return true;
                }
            }

            return false;
        }
    }
}

